; Data storage area at the end of this code was increased by 1 byte ; over the original "NMI ASM 2.asm" source file used in 90's. ; This extra byte (data+26) is used for storing the information ; regarding Interrupt enable/disable state before NMI ($00 if disabled, $01 if enabled). ; I only recently found out that when a LD A,R or LD A,I is executed, ; the state of IFF2 is copied to the parity flag, where it can be tested or stored. ; Back then I guess I didn't have this information. ORG $386F ; DISP $8000 ; OPUS specific pseudoinstruction, not needed here stack EQU $3C00 ; Stack will be inside BASIC unused empty area ; ************** Header info storage area (17 bytes) **************** ; T |---------- name (10 chars) ----------| length start Par2 header DEFB $03,$43,$52,$41,$43,$4B,$45,$52,$20,$AA,$AF,$00,$1B,$00,$40,$FF,$FF ; #### This is the start address of the actual NMI routine ($3880) #### ; #### At $0066 in the BASIC area we need to modify 3 bytes: $C3 $80 $38 #### LD (data), BC ; \ LD (data+2), DE ; | LD (data+4), HL ; | LD (data+6), SP ; | LD (data+8), IX ; | Save register contents to ... LD (data+10) IY ; | EXX ; | ... data storage area at the end of this code LD (data+12), BC ; | LD (data+14), DE ; | LD (data+16), HL ; | EXX ; / LD SP, stack ; Set stack inside BASIC unused empty area (with BASIC area R/W in hardware) EX AF, AF' PUSH AF ; Save AF' contents on stack EX AF AF' PUSH AF ; Save AF contents on stack POP HL LD (data+18), HL ; Copy AF contents to (data+18) POP HL LD (data+20), HL ; Copy AF' contents to (data+20) LD SP, (data+6) ; Restore initial SP contents (previously saved) POP HL ; When NMI was activated, the current exec addr (PC contents) ... LD (data+22), HL ; ... was pushed on stack. Now we read it and save it to (data+22) LD SP, stack-4 ; Set stack back to BASIC unused area, below the previously saved contents of AF ; -------------- NEWLY ADDED CODE FOR SAVING THE INTERRUPT ENABLE FLIP-FLOP STATE ------------------ LD A, $00 ; \ Assume maskable interrupts were disabled before NMI LD (data+26), A ; / and save the proper value ($00) in the storage area ; -------------------------------------------------------------------------------------------------- LD A, R LD (data+24), A ; Save R contents to (data+24) LD A, I LD (data+25), A ; Save I contents to (data+25) ; -------------- NEWLY ADDED CODE FOR SAVING THE INTERRUPT ENABLE FLIP-FLOP STATE ------------------ ; When a Load Register A with Register I (LD A, I) instruction ; or a Load Register A with Register R (LD A, R) instruction ; is executed, the state of IFF2 is copied to the parity flag ; where it can be tested or stored. We use this trick on next line: CALL PE IFF_1 ; If maskable interrupts were enabled before NMI, ; save the proper value in the "data" storage area ; -------------------------------------------------------------------------------------------------- LD A, $00 ; ##### The purpose of next 2 lines is to avoid modifying the IFF2 \ ; ##### interrupt flip-flop's state until returning from this NMI routine so the pre-NMI \ ; ##### interrupt state can be restored correctly when this code ends with RETN. ; ##### While this NMI routine executes, the maskable interrupts are disabled (IFF1=0) \ ; ##### so the tape saving routine will not be disturbed by interrupts anyway. LD ($04D4), A ; This replaces a "DI" instr. opcode in the SA-FLAG routine with a NOP LD ($054F), A ; This replaces a "EI" instr. opcode in the SA/LD-RET routine with a NOP LOOP LD A, $EF ; \ IN A, ($FE) ; | Test the F1 key (keyboard lines KA12 and K5) BIT 5, A ; / CALL Z, ONE ; Call "ONE" if F1 pressed (save screen as data block preceded by a standard header) LD A, $DF ; \ IN A, ($FE) ; | Test the F2 key (keyboard lines KA13 and K5) BIT 5, A ; / CALL Z, TWO ; Call "TWO" if F2 pressed (save registers + 48K user mem) LD A, $BF ; \ IN A, ($FE) ; | Test the F3 key (keyboard lines KA14 and K5) BIT 5, A ; / CALL Z, THREE ; Call "THREE" if F3 pressed (save registers + 48K user mem minus screen) LD A, $7F ; \ IN A, ($FE) ; | Test the F4 key (keyboard lines KA15 and K5) BIT 5, A ; / JR Z, FOUR ; Jump to "FOUR" if F4 pressed (return from this NMI routine) JR LOOP ONE LD IX, header ; Base addr of data to save (header) LD DE, $0011 ; Length of data to save (17 bytes) LD A, $00 ; Flag for saving ($00 for header block) CALL $04C2 ; Call the SA-BYTES routine to save the header CALL F1 ; Wait for F1 key LD IX, $4000 ; Base addr of data to save (screen) LD DE, $1B00 ; Length of data to save (screen including attrs) LD A, $FF ; Flag for saving ($FF for data block) CALL $04C2 ; Call the SA-BYTES routine to save the screen RET TWO CALL FF1 ; Save the registers' contents to tape with 00 flag (like a header) CALL F2 ; Wait for F2 key LD IX, $4000 ; Base addr of data to save (whole 48K user mem) LD DE, $C000 ; Length of data to save (48K) LD A, $FF ; Flag for saving ($FF for data block) CALL $04C2 ; Call the SA-BYTES routine to save the whole 48K user mem RET THREE CALL FF1 ; Save the registers' contents to tape with 00 flag (like a header) CALL F3 ; Wait for F3 key LD IX, $5B00 ; Base addr of data to save (48K user mem less the screen+attrs data) LD DE, $A500 ; Length of data to save (41.25K) LD A, $FF ; Flag for saving ($FF for data block) CALL $04C2 ; Call the SA-BYTES routine to save RET FOUR LD BC, (data) ; \ LD DE, (data+2) ; | LD HL, (data+4) ; | LD IX, (data+8) ; | LD IY, (data+10) ; | EXX ; | LD BC, (data+12) ; | Restore register contents (except for SP) from ... LD DE, (data+14) ; | ... data storage area at the end of this code LD HL, (data+16) ; | EXX ; | LD A, (data+24) ; | LD R, A ; | LD A, (data+25) ; | LD I, A ; / LD A, $F3 ; \ Restore the "DI" instruction opcode LD ($04D4), A ; / within system SA-FLAG routine LD A, $FB ; \ Restore the "EI" instruction opcode LD ($054F), A ; / within system SA/LD-RET routine POP AF ; Restore pre-NMI AF register contents EX AF, AF' POP AF ; Restore pre-NMI AF' register contents EX AF, AF' ; Set pre-NMI AF as current registers LD SP, (data+6) ; Restore pre-NMI Stack Pointer contents RETN ; ********* Return from this NMI routine ********** FF1 LD IX, data ; Base addr of data to save (previously saved registers' contents) LD DE, 27 ; Length of data to save (27 bytes) LD A, $00 ; Flag for saving ($00 like for header block) CALL $04C2 ; Call the SA-BYTES routine to save the registers) RET F1 LD A, $EF ; \ IN A, ($FE) ; | Test the F1 key (keyboard lines KA12 and K5) BIT 5, A ; / RET Z ; Return if F1 pressed JR F1 F2 LD A, $DF ; \ IN A, ($FE) ; | Test the F2 key (keyboard lines KA13 and K5) BIT 5, A ; / RET Z ; Return if F2 pressed JR F2 F3 LD A, $BF ; \ IN A, ($FE) ; | Test the F3 key (keyboard lines KA14 and K5) BIT 5, A ; / RET Z ; Return if F3 pressed JR F3 ; -------------- NEWLY ADDED CODE FOR SAVING THE INTERRUPT ENABLE FLIP-FLOP STATE ------------------ IFF_1 LD A, $01 ; In case interrupts were enabled before NMI \ LD (data+26), A ; save this information in the storage area RET ; -------------------------------------------------------------------------------------------------- data DEFS 27 ; Storage space for saving the CPU registers' contents at NMI ; ############## Allocation map for "data" storage space ################## ; ; data+$00 BC ; data+$02 DE ; data+$04 HL ; data+$06 SP ; data+$08 IX ; data+$0A IY ; data+$0C BC' ; data+$0E DE' ; data+$10 HL' ; data+$12 AF ; data+$14 AF' ; data+$16 PC ; data+$18 R ; data+$19 I ; data+$1A Interrupt enable/disable state before NMI ($00 if disabled, $01 if enabled) ; ############## HOW TO USE THIS CODE - FULL DOCUMENTATION ################ ; ; This program is a customized NMI routine to be placed in the empty unused area ; of the standard ZX Spectrum 16KB Basic interpreter. ; It is used for "cracking" Spectrum programs (games etc.) that have a "protected" ; non standard tape recording format. "Cracking" here means saving the program code ; after it finished loading, together with the CPU registers at NMI time and the ; CPU interrupt state. Next, this code can be transferred to a CP/M floppy disk as ; a CP/M .COM program that when executed switches to Basic mode and runs the "cracked" ; Spectrum program, picking up where it left when NMI was activated. ; The usage of this program is as follows: ; Under a Spectrum Basic having this NMI routine, load the program to be cracked from tape. ; Then switch off the write protection for the Basic DRAM area and push the NMI button. ; At this moment, the menu of this NMI routine is activated, which will respond to 4 keys: ; F1 - saves the screen in Spectrum "Bytes" format, with a standard header ; F2 - saves a 27 byte headerless block (markbyte $00) containing the data area ; at the end of this routine, followed by another headerless block (markbyte $FF) ; containing the $4000-$FFFF memory area. ; F3 - saves a 27 byte headerless block (markbyte $00) containing the data area ; at the end of this routine, followed by another headerless block (markbyte $FF) ; containing the $5B00-$FFFF memory area. ; F4 - returns to the Spectrum program interrupted by NMI. ; The 27 byte headerless block contains, in order, the CPU registers' contents: ; BC DE HL SP IX IY BC' DE' HL' AF AF' PC R I IFF ; where IFF is the interrupt flip flop (0 if maskable interrupts disabled, 1 if enabled). ; In order to copy a Spectrum program from tape to a CoBra CP/M floppy disk, proceed as follows: ; - Copy the NMI saved (by F2 or F3 keys) program data block from tape to disk using the CP/M ; utility KID.COM ; - Under CP/M, run the command "ZSID LOADER.COM" ; - At addresses $0103, $0105, $0107, $0109, using the "S" command in ZSID, enter in order: ; - the loading address (in Basic) for the NMI saved data block ($4000 if saved by F2, ; or $5B00 if saved by F3) ; - the NMI saved data block length ($C000 if saved by F2, or $A500 if saved by F3) ; - the start address in Basic (the equivalent under Basic of "adr") ; - the SP register contents which will be "adr", where "adr" will be specified next: ; - Using the "I filename" and "R200" commands in ZSID, load the NMI saved data block from disk ; and in it look for an empty area (zeros maybe?) of at least $40 bytes. Within this area keep ; 4 bytes free at the beginning (for a temporary stack used by the two PUSH HL instructions below) ; and then at the very next address (be that "adr") enter the following code: ; LD A, <R> ; LD R, A ; LD A, <I> ; LD I, A ; LD HL, <AF'> ; PUSH HL ; LD HL, <AF> ; PUSH HL ; POP AF ; EX AF, AF' ; POP AF ; EX AF, AF' ; LD BC, <BC> ; LD DE, <DE> ; LD HL, <HL> ; LD SP, <SP>+2 ; LD IX, <IX> ; LD IY, <IY> ; EXX ; LD BC, <BC'> ; LD DE, <DE'> ; LD HL, <HL'> ; EXX ; EI/DI ;if (data+$1A)=$01/$00 ; * IM 1 ;(or 2 or 3) ; JP <PC> ; The EI instruction will be used if (data+$1A)=$01, or the DI instruction if (data+$1A)=$00. ; The instruction flagged by * is only used if required. Since the Z80 Interrupt Mode cannot be ; tested and saved, the proper IM n instruction will have to be determined by trial and error. ; Try without the IM instruction first. If it works, ok, if it doesn't, try IM 1, IM 2, IM 3 ; until the program runs correctly